Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags. |
Create an rqt_bag Plugin
Description: Create a custom visualization for rqt_bagKeywords: rqt_bag bag data rqt
Tutorial Level: INTERMEDIATE
Contents
Let's say you have bags of data and you want to be able to visualize them. rqt_bag gives you the ability to scroll through the recorded messages and visualize the raw message values. However, often may want something more visual, or to do some post-processing on the raw message. For that, you can write an rqt_bag plugin, using the proof-of-concept Python plugin system. That way you can go from a simple visualization of the messages...
to something like this:
Package Setup
We're going to create a package called rqt_bag_diagnostics_demo. Insert the following into your package.xml.
1 <depend>diagnostic_msgs</depend>
2 <depend>rqt_bag</depend>
3 <export>
4 <rqt_bag plugin="${prefix}/plugins.xml"/>
5 </export>
Since we'll be making a python library, we'll need the standard Python setup. In your CMake
catkin_python_setup()
In setup.py
from distutils.core import setup from catkin_pkg.python_setup import generate_distutils_setup package_info = generate_distutils_setup( packages=['rqt_bag_diagnostics_demo'], package_dir={'': 'src'} ) setup(**package_info)
Finally, we're going to define the plugin in an xml file called plugins.xml (as referenced in the package.xml)
1 <library path="src">
2 <class name="DiagnosticBagPlugin"
3 type="rqt_bag_diagnostics_demo.the_plugin.DiagnosticBagPlugin"
4 base_class_type="rqt_bag::Plugin">
5 <description>
6 </description>
7 </class>
8 </library>
The name is the name of the class we'll create. The type is the way we would import the class in Python, i.e. package_name.name_of_file.class_name
Defining the Plugin
As with all Python libraries, we'll make sure a (blank) src/package_name/__init__.py file exists. As referenced in the plugins.xml, all the code that follows will be in src/rqt_bag_diagnostics_demo/the_plugin.py.
First, the core Plugin class.
1 from rqt_bag.plugins.plugin import Plugin
2 from python_qt_binding.QtCore import Qt
3 from diagnostic_msgs.msg import DiagnosticStatus
4
5
6 def get_color(diagnostic):
7 if diagnostic.level == DiagnosticStatus.OK:
8 return Qt.green
9 elif diagnostic.level == DiagnosticStatus.WARN:
10 return Qt.yellow
11 else: # ERROR or STALE
12 return Qt.red
13
14
15 class DiagnosticBagPlugin(Plugin):
16 def __init__(self):
17 pass
18
19 def get_view_class(self):
20 return None
21
22 def get_renderer_class(self):
23 return None
24
25 def get_message_types(self):
26 return ['diagnostic_msgs/DiagnosticStatus']
Here we have some basic imports, and helper function that we'll use later, and a class that defines the three parts of an rqt_bag plugin.
view_class - a.k.a. TopicMessageView - A separate panel that can be used for viewing individual messages.
renderer_class - a.k.a. TimelineView - A tool for drawing onto the timeline view of the bag data.
message_types - An array of strings that define what message types this plugin can be used for. You can return ['*'] for it to apply to all messages.
Since we return None for the first two methods, this plugin won't do anything. We'll tackle each of these separately.
TopicMessageView
Version 1
We're going to create a class that extends the TopicMessageView class.
Here we define two things. The name string defines what we'll see in the menu of rqt_bag. The message_viewed method defines what to do when the message is selected. So here, we'll just print the message to terminal for now.
We need to hook this class we've created into the plugin infrastructure, and for that, we return the class object itself in the get_view_class method.
To see this in action, open up the provided bag file, and right click on the diagnostic track. It will give you three options under the "View": Raw, Plot and our "Awesome Diagnostic." Clicking this should open a panel and you can scroll through the messages and watch them print.
Version 2
TopicMessageView is itself an extension of a QObject. There's lots of things you could do with this using all the might and power of Qt. This is not a python Qt tutorial sadly. So we're going to just add a simple QWidget and draw on it.
1 class DiagnosticPanel(TopicMessageView):
2 name = 'Awesome Diagnostic'
3
4 def __init__(self, timeline, parent, topic):
5 super(DiagnosticPanel, self).__init__(timeline, parent, topic)
6 self.widget = QWidget()
7 parent.layout().addWidget(self.widget)
8 self.msg = None
9 self.widget.paintEvent = self.paintEvent
10
11 def message_viewed(self, bag, msg_details):
12 super(DiagnosticPanel, self).message_viewed(bag, msg_details)
13 _, self.msg, _ = msg_details
14 self.widget.update()
15
16 def paintEvent(self, event):
17 self.qp = QPainter()
18 self.qp.begin(self.widget)
19
20 rect = event.rect()
21
22 if self.msg is None:
23 self.qp.fillRect(0, 0, rect.width(), rect.height(), Qt.white)
24 else:
25 color = get_color(self.msg)
26 self.qp.setBrush(QBrush(color))
27 self.qp.drawEllipse(0, 0, rect.width(), rect.height())
In the constructor, we create a QWidget and override its paintEvent method. Now when we get a message with message_viewed, we save it, and update the widget, which will in turn call our paintEvent. Before a message is selected, we'll just paint a white rectangle. Otherwise, we'll draw a circle, using our handy helper method to relate the color to what level the diagnostic is at.
TimelineRenderer
Version 1
To draw on the timeline, we extend the TimelineRenderer class.
1 class DiagnosticTimeline(TimelineRenderer):
2 def __init__(self, timeline, height=80):
3 TimelineRenderer.__init__(self, timeline, msg_combine_px=height)
4
5 def draw_timeline_segment(self, painter, topic, start, end, x, y, width, height):
6 painter.setBrush(QBrush(Qt.blue))
7 painter.drawRect(x, y, width, height)
You can customize how tall the message's portion of the timeline is with the msg_combine_px parameter. The key method to override is the draw_timeline_segment method which gives you potions of the timeline to draw. For now we'll just draw blue rectangles on each segment.
Just like the message view, you also have to edit the plugin to return your class.
To view this, you have to enable "Thumbnails" (a misleading name) in the rqt_bag gui.
Version 2
Okay, now we actually want to customize how the messages are drawn in the timeline based on the messages themselves. For that, there's a wonky bunch of magical incantations you need to read the messages out of the bag files.
1 def draw_timeline_segment(self, painter, topic, start, end, x, y, width, height):
2 bag_timeline = self.timeline.scene()
3 for bag, entry in bag_timeline.get_entries_with_bags([topic], rospy.Time(start), rospy.Time(end)):
4 topic, msg, t = bag_timeline.read_message(bag, entry.position)
5 color = get_color(msg)
6 painter.setBrush(QBrush(color))
7 painter.setPen(QPen(color, 5))
8
9 p_x = self.timeline.map_stamp_to_x(t.to_sec())
10 painter.drawLine(p_x, y, p_x, y+height)
Using the topic, start and end parameters of the method, we can get the bag entries that correspond with this segment of the timeline. We can then get the actual message and use it to draw. Here we are drawing a line based on the level of the diagnostic message. We can automatically figure out where to draw the message horizontally using the map_stamp_to_x method.
The alternative to this weird way of accessing the messages is to Timeline Cache like the ImageTimelineViewer does, but figuring that out is left as an exercise to the reader.